/*
 * Copyright (c) 2005, 2014 vacoor
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 */
package org.vacoor.nothing.security.shiro.filter;

import org.apache.shiro.ShiroException;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.session.Session;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Initializable;
import org.apache.shiro.util.StringUtils;
import org.apache.shiro.web.filter.AccessControlFilter;
import org.apache.shiro.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import java.io.Serializable;
import java.util.UUID;


/**
 * 单一 Session Filter, 保证登录后(authorization/rememberMe)的用户只有一个Session有效
 * 第二次登录时, 第一次登录失效
 * <p/>
 * 注意:
 * 如果使用该 Filter, 且重写了 UserPrincipal 的toString() 需要保证toString()能够唯一标识用户
 * <p/>
 * 是否继续执行 filterChain 在AccessControlFilter中 onPreHandle 定义:
 * return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
 * <p/>
 *
 * @author vacoor
 */
public class SessionLimitFilter extends AccessControlFilter implements Initializable {
    private static final String USER_TOKEN_KEY_PREFIX = SessionLimitFilter.class.getName() + "_USER_TOKEN_";
    private static final String DEFAULT_SESSION_CONTROL_CACHE_NAME = "shiro-sessionControlCache";

    private String sessionControlCacheName = DEFAULT_SESSION_CONTROL_CACHE_NAME;
    private CacheManager cacheManager;
    private String kickOutUrl;
    // private int maxSessionOnePeer;

    @Override
    public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        Subject subject = getSubject(request, response);

        // do next filter if not login
        // if (!subject.isAuthenticated() && !subject.isRemembered()) {
        if (subject.getPrincipal() == null) {
            return true;
        }

        // principal.toString() 必须唯一, 这里的Principal#toString() 有可能被重写,而导致多个用户tokenKey相同
        String uid = USER_TOKEN_KEY_PREFIX + subject.getPrincipal();

        // 这里存在 session 中, 也可以存在cookie中, 踢出时删除cookie
        Session session = subject.getSession();
        Cache<Serializable, Serializable> cache = getSessionControlCache();

        String validToken = (String) cache.get(uid);
        String sessionToken = (String) session.getAttribute(uid);

        // new login
        if (sessionToken == null || validToken == null) {
            String token = generateToken();

            session.setAttribute(uid, token);
            cache.put(uid, token);
            validToken = sessionToken = token;
        }

        return validToken.equals(sessionToken);
    }

    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        Subject subject = getSubject(request, response);
        Session session = subject.getSession(false);

        subject.logout();
        if (session != null) {
            session.stop();
        }
        saveRequest(request);

        String redirectUrl = StringUtils.hasText(kickOutUrl) ? kickOutUrl : getLoginUrl();
        WebUtils.issueRedirect(request, response, redirectUrl);
        return false;
    }

    protected String generateToken() {
        return UUID.randomUUID().toString();
    }

    // ----- getter / setter
    public String getKickOutUrl() {
        return kickOutUrl;
    }

    public void setKickOutUrl(String kickOutUrl) {
        this.kickOutUrl = kickOutUrl;
    }

    protected Cache<Serializable, Serializable> getSessionControlCache() {
        CacheManager mgr = getCacheManager();
        String name = getSessionControlCacheName();
        return mgr.getCache(name);
    }

    // ------------------
    public CacheManager getCacheManager() {
        return cacheManager;
    }

    public void setCacheManager(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public String getSessionControlCacheName() {
        return sessionControlCacheName;
    }

    public void setSessionControlCacheName(String sessionControlCacheName) {
        this.sessionControlCacheName = sessionControlCacheName;
    }

    @Override
    public void init() throws ShiroException {
        if (cacheManager == null) {
            throw new IllegalStateException("CacheManager 没有设置");
        }
    }
}